<?php
/**
 * assets.class
 * 
 * @version:	0.1
 * @package:	core
 * @author:		alex_ez
 * @created:	24 apr 2010
 *
 * @copyright:	(c) 2010 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

class assets_exception extends Exception { }

class assets extends singleton
{
	// php singleton wrapper
	static public function &instance( ) { return parent::instance( __CLASS__ ); }
	
	protected $config = null;
	protected $assets = array( );
	protected $version = 0;
	protected $mode = null;
	protected $priority = 10;

	protected $items = array( );
	protected $printed = array( );
	
	protected $log = array( );

	protected $file_shortcuts = array(
		'js' => '/js/',
		'css' => '/css/',
		'googleapis' => 'http://ajax.googleapis.com/ajax/libs/',
		'microsoftapis' => 'http://ajax.microsoft.com/ajax/',
		);

	protected $patterns = array(
		'js'			=> '<script src="%1$s" type="text/javascript"></script>',
		'js_inline'		=> "<script type=\"text/javascript\">\n//<![CDATA[\n%1\$s\n//]]>\n</script>",
		'js_domready'	=> "addLoadEvent(function(){\n%1\$s;\n});",
		'css'			=> '<link rel="stylesheet" type="text/css" href="%1$s" />',
		'css_inline'	=> "<style>\n%1\$s\n</style>",
		'image'			=> '<img src="%1$s" style="display:none" />',
		);

	protected $preload_js = array(
		'js_domready' => "window.addLoadEvent=function(f){if(typeof jQuery!='undefined'){jQuery(document).ready(f);}else{var of=window.onload||function(){};window.onload=function(){of();f();}}};\n"
	);

	protected $preg_strict = '@^(js:|css:)?https?://@i';
	
	protected function __construct( )
	{
		$this ->config	= config::instance( );
		$this ->assets = $this ->config ->assets;
		$this ->set_version( $this ->config ->assets__version );
		$this ->set_mode( $this ->config ->assets__mode );
	}
	
	public function strict_data($data)
	{
		if(preg_match($this->preg_strict,$data))
			return true;
		return false;
	}
	
	public function set_version($version) {
		$this ->version = $version;
	}
	
	public function version() {
		return _::pick( $this ->version, 0 );
	}
	
	public function set_mode($mode) {
		$this ->mode = $mode;
	}
	
	public function mode() {
		return _::pick($this->mode, 'inline');
	}
	
	public function priority($priority) {
		$this->priority = $priority;
		return $this;
	}

	/**
	 * Parsing string to array
	 * 
	 * @param string $s
	 * @return array Array with strings
	 */
	public function parse_string($s) {
		return array_unique(array_filter(
			explode( ' ', str_replace( ',', ' ', $s ) )
			));
	}

	public function display() {
		return $this->render();
	}
	
	public function render() {

		if (bc::enabled()) bc::group('assets');

		$r = '';
		uasort($this->items,
			create_function('$a,$b','if($a["priority"]==$b["priority"]) return 0; return ($a["priority"]<$b["priority"])?1:-1;'));

		foreach($this->preload_js as $key => $value) {
			$r .= $this ->render_item('js_inline', md5($value), false, trim($value,";\r\n "));
		}
		
		foreach($this->items as $k => $v) {
			$file = substr($k,strpos($k,':')+1);
			$r .= $this ->render_item( $v['type'], $file, max( $v['versions'] ), $v['data'] );
			unset( $this->items[$k] );
		}

		if (bc::enabled()) bc::groupEnd('assets');
		
		return $r;
	}


	public function render_item($type, $m, $v = 0, $data = null) {

		if( $this->strict_data($m) || strpos($type,'_inline') ) {
			if( isset( $this->printed[$m] ) )
				return;
			$this->printed[$m] = $v;
			if (bc::enabled()) bc::log( '+ ' . $type . ': ' . (is_array($data) ? join(',',$data) : $data) );

			return vsprintf( $this->patterns[$type], $data )."\n";
		}

		$r = '';

		$pathtpl = 'site/%2$s/%1$s.%2$s';
		$hreftpl = $v ? sprintf('/%%2$s/%%1$s-%d.%%2$s',$v) : '/%2$s/%1$s.%2$s';

		// action!
		$nsis = mb_strpos( $m, '::' ) !== false;
		$ns = mb_substr( $m, 0, mb_strpos( $m, '::' ) );// namespaces have own files
		$f = str_replace( '::', '/', $m );

		//if (bc::enabled()) bc::log( array( $m, $ns, $f ), 'guesses' );
		$files = array( );
		foreach( array( $ns, $f ) as $guess )
		if( $guess ) {
			$filepath = sprintf( $pathtpl, $guess, $type );
			$filehref = sprintf( $hreftpl, $guess, $type );
			$files[ $filepath ] = array( 'href' => $filehref, 'type' => $type );
		}

		foreach( $files as $path => $file )
		if( is_readable( $path ) ) {
			if( isset( $this ->printed[$path] ) ) continue;
			$this ->printed[$path] = $v;
			if (bc::enabled()) bc::log( '+ ' . $type . ': ' . $path . ' -> ' . $file['href'] );

			$r .= sprintf( $this ->patterns[$type], $file['href'] )."\n";
		}
		else {
			if (bc::enabled()) if (!strpos($path,'.css')) bc::warn( '? ' . $path );
			$this->log( sprintf( '%s not found', $path ) );
		}
		
		return $r;
	}
	
	public function fetch() {
		return $this->render();
	}
	
	public function flush() {
		echo $this->fetch();
	}




	/**
	 *
	 * @param <type> $type
	 * @param string $file
	 * @param numeric $version
	 */
	public function push( $type, $file, $version, $data = null )
	{
		if(isset($this->items[$file]))
			$item =& $this->items[$file];
		else
			$item = array('priority' => $this->priority, 'type' => $type);

		$item['data'] = is_null($data) ? array($file) : $data;
		$item['priority'] = max($item['priority'],$this->priority);
		$item['versions'][] = $version;
		$this->items[$type.':'.$file] = $item;
		
		$this->priority -= 0.01;
	}
	
	/**
	 * Append some data to storage (from which it placed to page)
	 *
	 * @param string $type js, css, set or image
	 * @param mixed $data list with filenames. "file1 file2", "dir::file1 file2" and so on
	 * @param numeric $version files version. 0 by default (that means only one version)
	 */
	public function append( $type, $data, $version = 0 )
	{
		if( strpos($type,'_inline')===false ) {
			if( is_string($data) )
				$data = $this ->parse_string( $data );
		
			foreach( array_keys($data) as $dk )
				foreach( $this->file_shortcuts as $k => $fs )
					if( strpos($data[$dk],$k.':')===0 )
						$data[$dk] = $fs . substr($data[$dk],strlen($k)+1);
		}

		switch( $type ) {
		
		case 'js_inline':
		case 'css_inline':
			//$data = join(' ',$data);
			$file = md5($data);
			$this->push($type,$file,0,array(trim($data,";\r\n ")));
			break;
		
		case 'js':
			foreach( $data as $file ) {
				$this ->push( 'js', $file, $version );
				if(!$this->strict_data($file))
					$this ->push( 'css', $file, $version );
			}
			break;

		case 'css':
			foreach( $data as $file ) {
				$this ->push( 'css', $file, $version );
			}
			break;
		
		case 'image':
			foreach( $data as $file ) {
				$this ->push( 'image', $file, $version );
			}
			break;
		}
	}
	
	/**
	 * Shortcuts for ::append( ... )
	 */
	public function __call( $m, $p ) {
		
		if(!isset($p[1])) $p[1] = 0;

		switch($m) {
			case 'js':
			case 'css':
			case 'css_inline':
			case 'image':
				$this->append( $m, $p[0], $p[1] );
				return $this;

			case 'js_domready':
				$p[0] = sprintf($this->patterns[$m], trim($p[0],";\r\n "));
				// fall down
			case 'js_inline':
				$this->append('js_inline', $p[0], $p[1]);
				return $this;

			default:
				trigger_error( 'Call to undefined method assets::'.$m.'()', E_USER_ERROR );
		}
	}
	
	/**
	 * 
	 */
	public function asset( $asset, $v = 0 )
	{
		if(!isset($this->assets[$asset]) ||
		   !is_array($this->assets[$asset])) {
			$this->log( sprintf( 'set %s not found', $asset ) );
			return $this;
		}
		
		$asset = $this->assets[$asset];
		foreach( $asset as $type => $data )
			$this->$type($data, $v);

		return $this;
	}

	/**
	 * Assets logger
	 * @param string $s
	 */
	protected function log( $s )
	{
		$this ->log[] = $s;
	}

	/**
	 * Assets log printer
	 */
	public function printlog( )
	{
		if( empty( $this ->log ) )
			return;
		
		if (bc::enabled()) bc::info( $this ->log, 'assets' );
		$this ->log = array( );
	}
	
}


?>